﻿using System;
using System.Collections.Generic;
using System.Text;
using llAma.Base;
using MySql.Data.MySqlClient;
using llAma.Backend.Interface;
using llAma;
using System.Threading;
using System.Diagnostics;
using llamaServer;
using dAmn;
using System.Text.RegularExpressions;
using System.Net;

namespace socialdeveloperBackend
{
    public partial class SDBackend : BackendBase
    {
        static SDBackend mCurrentInstance = null;
        public static IBackend CurrentInstance
        {
            get { return mCurrentInstance; }
        }
        public static MySqlDataReader GetDataReader(string commandText, MySqlConnection dbConnection, params MySqlParameter[] param)
        {
            MySqlCommand command = new MySqlCommand(commandText, dbConnection);
            for (int x = 0; x < param.Length; x++)
            {
                command.Parameters.Add(param[x]);
            }
            return command.ExecuteReader();
        }
        public static MySqlDataReader ExecuteReader(MySqlConnection conn, string sqlStatement, params MySqlParameter[] args)
        {
            MySqlCommand cmd = new MySqlCommand();
            for (int x = 0; x < args.Length; x++)
            {
                cmd.Parameters.Add(args[x]);
            }
            cmd.Connection = conn;
            cmd.CommandText = sqlStatement;
            //cmd.Prepare();
            return cmd.ExecuteReader(System.Data.CommandBehavior.CloseConnection);
        }
        public static int ExecuteNonQuery(string sqlStatement, params MySqlParameter[] args)
        {
            using (MySqlConnection conn = CreateConnection())
            {
                using (MySqlCommand cmd = new MySqlCommand(sqlStatement, conn))
                {
                    for (int x = 0; x < args.Length; x++)
                    {
                        cmd.Parameters.Add(args[x]);
                    }
                    int ret = cmd.ExecuteNonQuery();
                    return ret;
                }
            }
        }
        private void PreCacheChannels()
        {
            //using (MySqlConnection conn = CreateConnection())
            //{
            //    using (MySqlDataReader dr = ExecuteReader(conn, "SELECT * FROM tbl_namespaces"))
            //    {
            //        Console.WriteLine("Precaching...");
            //        int lCount = 0;
            //        while (dr.Read())
            //        {
            //            Console.CursorLeft = 0;
            //            Console.Write(lCount);
            //            lCount++;
            //            string p1, p2;
            //            p1 = (string)dr["toplevel"];
            //            p2 = (string)dr["name"];
            //            this.OnChannelRequest(string.Join(":", new string[] { p1, p2 }));
            //        }
            //        Console.Write(" Total records loaded.");
            //    }
            //}
        }
        protected override void OnHandleChannelPacket(Packet p, IConnection sender)
        {
            base.OnHandleChannelPacket(p, sender);
        }
        ServerCache<string, ChatChannel> channelCache = new ServerCache<string, ChatChannel>(new TimeSpan(0, 0, 15));
        public IChannel LookupChatChannel(string fullNamespace)
        {
            string key = fullNamespace.ToLower();
            ChatChannel ret = channelCache.GetCacheObject(key, delegate(string key2)
            {
                //We need to move this code to a static create method, and return null on failure.
                ChatChannel thisChannel = ChatChannel.GetChatChannel(fullNamespace);
                CacheEntry<ChatChannel> ce = new CacheEntry<ChatChannel>(thisChannel, new TimeSpan(0, 0, 15));
                ce.ShouldExpireThis = ChatChannel.ShouldExpire;
                return ce;

            }
            );
            //Its okay to return null.
            //if (ret == null) throw new InvalidNamespaceException();
            return ret;
        }
        ServerCache<string, PChatChannel> pchatCache = new ServerCache<string, PChatChannel>();
        private IChannel LookupPChatChannel(string user1, string user2)
        {

            string key = string.Join(":", new string[] { user1, user2 }).ToLower();
            IChannel ret = pchatCache.GetCacheObject(key, delegate(string k)
            {
                int cmpVar = string.Compare(user1, user2, true);
                if (cmpVar >= 0)
                    return new CacheEntry<PChatChannel>(null);
                {
                    IUserData u1, u2;
                    u1 = GlobalUserCache.GetUser(user1);
                    u2 = GlobalUserCache.GetUser(user2);
                    if (u1 == null || u2 == null)
                        return new CacheEntry<PChatChannel>(null);
                    user1 = u1.getInfo("username");
                    user2 = u2.getInfo("username");
                }
                PChatChannel p = PChatChannel.create(user1, user2);
                CacheEntry<PChatChannel> pc = new CacheEntry<PChatChannel>(p, new TimeSpan(0, 0, 15));
                pc.ShouldExpireThis = PChatChannel.ShouldExpire;
                return pc;
            }
            );
            return ret;
        }

        //static string connStr = "Server=127.0.0.1;Database=llama_server_db;Uid=root;Pwd=8ad6vz;";
        static string connStr
        {
            get
            {
                string cstr = Server.Config["db"]["connectionstring"];
                if (cstr.Trim() == "")
                {
                    cstr = "Server=127.0.0.1;Database=llama_server_db;Uid=root;Pwd=8ad6vz;";
                    Server.Config["db"]["connectionstring"] = cstr;
                    Server.SaveConfig();
                }
                return cstr;
            }
        }
        Timer pulseTimer;
        public SDBackend()
        {
            pulseTimer = new Timer(delegate(object o)
            {
                long tot = CacheStats.Hits + CacheStats.Misses;
                this.OnLogMessage(string.Format("Hit Ratio: {0:0%}, Miss Ratio {1:0%}", (double)CacheStats.Hits / tot, (double)CacheStats.Misses / tot));
                Process proc = Process.GetCurrentProcess();
                try
                {
                    this.OnLogMessage(string.Format("Memory usage: {0}kb (According to GC), {1}kb (According to OS).", (long)(GC.GetTotalMemory(false)/1024), (long)(proc.PrivateMemorySize64/1024)));
                }
                catch { }
            }, null, new TimeSpan(0, 1, 0), new TimeSpan(0, 1, 0));
            GlobalUserCache.BuildInitialCache();
            PreCacheChannels();
            SDBackend.mCurrentInstance = this;

            // this.HandleOnSeperateThread = false; This defaults to true when unset.
        }
        protected override llAma.Backend.Interface.IChannel OnChannelRequest(string full_namespace)
        {
            System.Threading.Thread.Sleep(0);
            string[] parts = full_namespace.Split(':');
            switch (parts[0])
            {
                case "pchat":
                    if (parts.Length != 3)
                        return null;
                    return LookupPChatChannel(parts[1], parts[2]);
                case "chat":
                    return LookupChatChannel(full_namespace);
                case "login":
                    return null;
                default:
                    return null;
            }
        }
        
        public static MySqlConnection CreateConnection()
        {
            while (threadCount > 15)
            {
                Debug.WriteLine(string.Format("Waiting for threadcount to drop, Currently {0}", threadCount));
                Thread.Sleep(100);
            }
            Interlocked.Increment(ref threadCount);
            MySqlConnection threadConn = new MySqlConnection(connStr);
            threadConn.Open();
            threadConn.StateChange += new System.Data.StateChangeEventHandler(threadConn_StateChange);
            return threadConn;
        }
        static int threadCount = 0;
        static void threadConn_StateChange(object sender, System.Data.StateChangeEventArgs e)
        {
            if (e.CurrentState == System.Data.ConnectionState.Closed)
            {
                Interlocked.Decrement(ref threadCount);
            }
        }
        protected override llAma.Backend.Interface.IUserData OnRequestAuthentication(string user, string token, Guid uuid)
        {
            IUserData d = GlobalUserCache.GetUser(user);
            if (d != null)
            {
                System.Threading.Thread.Sleep(0);
                if (!GlobalUserCache.Authenticate(d, token))
                    d = null;
            }
            return d;
        }

        protected sealed override string GetNamespace()
        {
            return "system:backend";
        }
        public static IConnection[] getConnectionsByName(string name)
        {
            return mCurrentInstance.GetUserConnections(name);
        }
        public static IConnection[] getConnectionsByIP(IPAddress ip)
        {
            return mCurrentInstance.GetUserConnections(ip);
        }
        protected sealed override void HandleWhoisPacket(string name, IConnection conn)
        {
            IConnection[] conns = GetUserConnections(name);
            //We process this in reverse. Don't worry, Its easy.
            bool showPchats = ((IBackend)this).GpcHandler.Demand(new PermissionRule("admin", 0, Comparison.NEQ), conn.CurrentUser.getInfo("gpc"));
            Packet outPkt = Packet.EmptyPacket;
            int trueCount = 0;
            for (int i = 0; i < conns.Length; i++)
            {
                if (!conns[i].Connected)
                {
                    conns[i].Drop("timed out");
                    continue;
                }
                trueCount++;
                IChannel[] chans = conns[i].AllChannels;
                outPkt.args.Clear();
                //First off, Its ns namespace.
                //Should we include pchats? For now we will filter them out, pending testing of putting them in.
                for (int j = 0; j < chans.Length; j++)
                {
                    if (!showPchats && chans[j].Namespace.ToLower().StartsWith("pchat:"))
                        continue; //Ignore this namespace.
                    outPkt.cmd = "ns";
                    outPkt.param = chans[j].Namespace;
                    outPkt.body = outPkt;
                }
                if (showPchats)
                {
                    outPkt.cmd = "agent";
                    outPkt.args["agent"] = conns[i].Agent;
                    outPkt.body = outPkt;
                    outPkt.args.Clear();
                }
                if (showPchats)
                {
                    outPkt.cmd = "debug";
                    outPkt.args["ip"] = conns[i].RemoteAddress.ToString();
                    outPkt.args["port"] = conns[i].RemotePort.ToString();
                    if(((IBackend)this).GpcHandler.Demand(new PermissionRule("debug", 0, Comparison.NEQ), conn.CurrentUser.getInfo("gpc")))
                    {
                        outPkt.args["uuid"] = conns[i].UUID.ToString();
                        outPkt.args["hashid"] = conns[i].GetHashCode().ToString();
                        outPkt.args["uuid_hash"] = conns[i].UUID.GetHashCode().ToString();
                    }
                    outPkt.body = outPkt;
                    outPkt.args.Clear();
                }
                //Now we write out the connection statistics.
                outPkt.cmd = "conn";
                outPkt.param = "";
                outPkt.args["online"] = ((int)((DateTime.Now - conns[i].LoggedIn).TotalSeconds)).ToString();
                outPkt.args["idle"] = ((int)((DateTime.Now - conns[i].LastPacket).TotalSeconds)).ToString();
                outPkt.body = outPkt;
            }
            if (trueCount == 0)
            {
                Packet p = EmptyRecvPacket;
                p.cmd = "get";
                p.param = "login:" + name;
                SendErrorPacketToClient(conn, p, "this user is not currently in any chatroom");
                return;
            }
            outPkt.cmd = "";
            outPkt.param = "";
            outPkt.args.Clear();
            string[] userVars = { "usericon", "symbol", "realname", "typename", "gpc" };
            for (int i = 0; i < userVars.Length; i++)
            {
                outPkt.args[userVars[i]] = conns[0].CurrentUser.getInfo(userVars[i]);
            }
            outPkt.body = outPkt;
            outPkt.cmd = "property";
            outPkt.param = "login:" + conns[0].CurrentUser.getInfo("username");
            outPkt.args.Clear();
            outPkt.args["p"] = "info";
            conn.Send(outPkt);
        }
        protected sealed override void OnKilledConnection(IConnection c)
        {
            GlobalUserCache.DropUser(c.CurrentUser);
        }
        protected sealed override void OnLogMessage(string message)
        {
            System.Threading.Thread.Sleep(0);
            IChannel chan = this.LookupChatChannel("chat:debug");
            if (chan == null) return;
            Packet p = EmptyRecvPacket;
            p.body = "msg main\nfrom=SERVER\n\n" + SanitizeUserInput(message);
            p.param = chan.Namespace;
            chan.SendToAll(p.ToString());
        }
        [PacketHandler("reload")]
        protected void handleReloadCommand(Packet p, IConnection conn)
        {
            if (!((IBackend)this).GpcHandler.Demand(new PermissionRule("admin", 0, Comparison.NEQ), conn.CurrentUser.getInfo("gpc")))
            {
                SendErrorPacketToClient(conn, p, "not privileged");
                return;
            }
            ChatChannel c = (ChatChannel)this.LookupChatChannel(p.param);
            if (c == null)
            {
                SendErrorPacketToClient(conn, p, "bad namespace");
                return;
            }
            OnLogMessage(string.Format("{0} forced {1} to reload from the databse.", conn.CurrentUser.getInfo("username"), p.param));
            c.ForceReload();
            SendErrorPacketToClient(conn, p, "ok");
        }
        Regex namespaceValidator = new Regex(@"^[A-Za-z0-9]+$");
        [PacketHandler("create")]
        protected void handleCreateCommand(Packet p, IConnection conn)
        {
            if (!((IBackend)this).GpcHandler.Demand(new PermissionRule("create", 0, Comparison.NEQ), conn.CurrentUser.getInfo("gpc")))
            {
                SendErrorPacketToClient(conn, p, "not privileged");
                return;
            }
            string[] parts = p.param.Split(':');
            //param is all we need.

            if (string.IsNullOrEmpty(p.param) ||
                parts.Length != 2 ||
                parts[1].Length > 20 ||
                !namespaceValidator.IsMatch(parts[1]) ||
                parts[0].ToLower() != "chat")
            {
                Packet o = new Packet();
                o.cmd = "join";
                o.param = p.param;
                SendErrorPacketToClient(conn, o, "bad namespace");
                return;
            }
            //Seems valid.

            IChannel c = LookupChatChannel(p.param);
            if (c != null)
            {
                //It exists? Ok. Just join then.
                p.cmd = "join";
                ((IHandlePackets)this).HandlePacket(p, conn);
                return;
            }
            //Good. Does not exist.
            ExecuteNonQuery("INSERT INTO tbl_namespaces (`toplevel`, `name`) VALUES (@tl, @name)",
                new MySqlParameter("@tl", parts[0]),
                new MySqlParameter("@name", parts[1]));
            string[] defaultPcNames = { "Banned", "Members", "Seniors", "Operators", "Founders" };
            int[] defaultPcOrders = { 1, 25, 50, 75, 99 };
            //INSERT INTO `llama_server_db`.`tbl_privclass` (`name`, `channel`, `order`) VALUES ('Banned', 5, 1);
            //INSERT INTO `llama_server_db`.`tbl_privclass` (`name`, `channel`, `order`) VALUES ('Members', 5, 25);
            //INSERT INTO `llama_server_db`.`tbl_privclass` (`name`, `channel`, `order`) VALUES ('Seniors', 5, 50);
            //INSERT INTO `llama_server_db`.`tbl_privclass` (`name`, `channel`, `order`) VALUES ('Operators', 5, 75);
            //INSERT INTO `llama_server_db`.`tbl_privclass` (`name`, `channel`, `order`) VALUES ('Founders', 5, 99);
            //Force a database lookup of our new record.
            ChatChannel thisChannel = ChatChannel.GetChatChannel(p.param);
            MySqlParameter channelID = new MySqlParameter("@cid", thisChannel.ID);
            MySqlParameter pcid = new MySqlParameter("@pcid", 0);
            MySqlParameter uid = new MySqlParameter("@userid", int.Parse(conn.CurrentUser.getInfo("id")));

            //Insert default privclasses
            for (int x = 0; x < defaultPcNames.Length; x++)
            {
                ExecuteNonQuery("INSERT INTO tbl_privclass (`name`, `channel`, `order`) VALUES ( @name, @cid, @order);",
                    new MySqlParameter("@name", defaultPcNames[x]),
                    new MySqlParameter("@cid", thisChannel.ID),
                    new MySqlParameter("@order", defaultPcOrders[x]));
            }
            using (MySqlConnection sqlConn = SDBackend.CreateConnection())
            {
                pcid.Value = 99;
                using (MySqlDataReader dr = SDBackend.GetDataReader(
                    "SELECT * FROM tbl_privclass WHERE `channel`=@cid AND `order`=@pcid",
                    sqlConn,
                    channelID,
                    pcid))
                {
                    //Make the creator a founder
                    while (dr.Read())
                    {
                        pcid.Value = dr["id"];
                        ExecuteNonQuery("INSERT INTO tbl_privclass_members (`id`, `memberid`) VALUES ( @pcid, @userid )",
                            pcid,
                            uid);
                    }
                }

                using (MySqlDataReader dr = SDBackend.GetDataReader("SELECT * FROM tbl_privclass WHERE channel = @cid", sqlConn, channelID))
                {
                    while (dr.Read())
                    {
                        int this_pc = (int)dr["id"];
                        // Install initial permissions.
                        switch ((int)dr["order"])
                        {
                            case 1:
                                InsertPermission(sqlConn, "join", 0, this_pc);
                                InsertPermission(sqlConn, "shownotice", 0, this_pc);
                                break;
                            case 25:
                                //No permissions, Use default.
                                break;
                            case 50:
                                InsertPermission(sqlConn, "topic", 1, this_pc);
                                InsertPermission(sqlConn, "kick", 1, this_pc);
                                break;
                            case 75:
                                InsertPermission(sqlConn, "topic", 1, this_pc);
                                InsertPermission(sqlConn, "title", 1, this_pc);
                                InsertPermission(sqlConn, "kick", 1, this_pc);
                                InsertPermission(sqlConn, "promote", 75, this_pc);
                                InsertPermission(sqlConn, "demote", 1, this_pc);
                                break;
                            case 99:
                                InsertPermission(sqlConn, "topic", 1, this_pc);
                                InsertPermission(sqlConn, "title", 1, this_pc);
                                InsertPermission(sqlConn, "kick", 1, this_pc);
                                InsertPermission(sqlConn, "promote", 99, this_pc);
                                InsertPermission(sqlConn, "demote", 1, this_pc);
                                InsertPermission(sqlConn, "admin", 1, this_pc);
                                break;

                        }
                    }
                }
            }


            //Create the channel one more time:
            //This forces an update of the Privclasses.
            thisChannel.ForceReload();
            //thisChannel = ChatChannel.GetChatChannel(p.param);
            channelCache.DeleteCacheObject(thisChannel.Namespace.ToLower());
            thisChannel = null;
            p.cmd = "join";
            ((IHandlePackets)this).HandlePacket(p, conn);
        }
        const string insertPermissionValueSQL = "INSERT INTO tbl_privclass_permissions ( `name`, `value`, `privclass_id`) VALUES ( @permname, @value, @pcid )";
        
        public static void InsertPermission(MySqlConnection conn, string name, int value, int pcid)
        {

            ExecuteNonQuery(insertPermissionValueSQL, new MySqlParameter("@permname", name),
                new MySqlParameter("@value", value), new MySqlParameter("@pcid", pcid));
        }
#if DEBUG
        [PacketHandler("debug")]
        protected void OnDebugPacket(Packet p, IConnection sender)
        {
            if (!SDBackend.CurrentInstance.GpcHandler.Demand(new PermissionRule("debug", 0, Comparison.NEQ), sender.CurrentUser.getInfo("gpc")))
            {
                sender.Drop("attempted to hack server");
                return;
            }
            string userName = sender.CurrentUser.getInfo("username");
            switch (p.param)
            {
                case "sql":
                    int rowsAffected = 0;
                    try
                    {
                        if (p.body.ToUpper().StartsWith("SELECT"))
                        {
                            using (MySqlConnection scon = CreateConnection())
                            {
                                using (MySqlDataReader dr = GetDataReader(p.body, scon, new MySqlParameter[0]))
                                {
                                    StringBuilder sb = new StringBuilder();
                                    if (!dr.HasRows)
                                    {
                                        OnLogMessage(string.Format("{0}: 0 row(s) were selected for query {1}", userName, p.body));
                                        break;
                                    }
                                    else
                                    {
                                        List<string> cols = new List<string>();
                                        sb.Append("<bcode>");
                                        for (int x = 0; x < dr.FieldCount; x++)
                                        {
                                            cols.Add(dr.GetName(x) + " (" + dr.GetDataTypeName(x) + ")");
                                        }
                                        sb.Append(string.Join(",\t", cols.ToArray()));
                                        while (dr.Read())
                                        {
                                            sb.Append("\n");
                                            for (int x = 0; x < dr.FieldCount; x++)
                                            {
                                                sb.AppendFormat("'{0}',\t", dr[x]);
                                            }
                                        }
                                        OnLogMessage(string.Format("{0}:\n{1}", userName, sb.ToString()));
                                    }
                                }
                            }
                        }
                        else
                        {
                            rowsAffected = ExecuteNonQuery(p.body, new MySqlParameter[0]);
                            OnLogMessage(string.Format("{0}: {1} row(s) were affected for query {2}", userName, rowsAffected, p.body));
                        }
                    }
                    catch (Exception e)
                    {
                        OnLogMessage(string.Format("{0}: An error occured while trying to execute the SQL\nThe exception was: {1}", userName, e.ToString()));
                        return;
                    }
                    break;
                case "reset":
                    if (p.body.Trim() != "YES")
                    {
                        OnLogMessage(string.Format("{0}: Are you sure you want to reset the server? Please put YES in the body to confirm.", userName));
                        return;
                    }
                    OnLogMessage(string.Format("{0}: Resetting the state is not currently implemented.", userName));
                    break;
                default:
                    OnLogMessage(string.Format("{1}: Unknown debug section: {0}", p.param, sender.CurrentUser.getInfo("username")));
                    return;
            }
        }
#endif
        protected override llAma.Interface.IGpc OnGPCRequest()
        {
            //This is only called once.
            return new SDGPC();
        }
    }
}
